-- MoonSaberLedger.lua

-- initialize saved variables as necessary

MoonSaberLedger_CharacterData = {}
--MoonSaberLedger_SharedData = {}

local function expose_table(table, sub_table_name)
  if table[sub_table_name] == nil then
    table[sub_table_name] = {}
  end

  return table[sub_table_name]
end

local auction_owned_counts
local transaction_log

local function setup_variables()
  auction_owned_counts = expose_table(MoonSaberLedger_CharacterData, 'auction_owned_counts')
  transaction_log = expose_table(MoonSaberLedger_CharacterData, 'transaction_log')
end

-- initialize session variables

local last_item_lock
local last_spellcast_succeeded

local merchant_window = false
local merchant_per_item_costs = {}

-- general utility functions

local function get_item_name_from_item_link(item_link)
  local i, j, item_name = string.find(item_link, '%|h(.*)%|%h')
  if item_name == nil then
    return
  end

  item_name = string.gsub(item_name, '%[', "")
  item_name = string.gsub(item_name, '%]', "")

  return item_name
end

-- 

local transaction_fields = {}
transaction_fields['buy'] = {'buy_price', 'output_name', 'output_count'}
transaction_fields['create'] = {'output_name', 'output_count'}
transaction_fields['refine'] = {'input_name', 'input_count', 'output_name', 'output_count'}
transaction_fields['sell'] = {'sell_price', 'input_name', 'input_count'}

local function record_transaction(transaction, transaction_done)
  local function check_transaction(k)
    if transaction[k] == nil then
      error('attempt to record transaction missing ' .. k .. ' attribute')
      return 1
    end
  end

  if check_transaction('type') then
    return
  end

  local required_fields = transaction_fields[transaction.type]
  if required_fields == nil then
    error('unrecognized transaction type ' .. transaction.type)
    return
  end

  for i, n in ipairs(required_fields) do
    if check_transaction(n) then
      return
    end
  end

  -- track counts after transaction

  local function update_final(item_name, count_name, total_name, count_sign) 
    if item_name then
      transaction[total_name] = GetItemCount(item_name, true)
      if not transaction_done then
        transaction[total_name] = transaction[total_name] + count_sign * transaction[count_name]
      end
    end
  end

  update_final(transaction.input_name, 'input_count', 'input_total', -1)
  update_final(transaction.output_name, 'output_count', 'output_total', 1)
  update_final(transaction.output2_name, 'output2_count', 'output2_total', 1)
  update_final(transaction.output3_name, 'output3_count', 'output3_total', 1)
  update_final(transaction.output4_name, 'output4_count', 'output4_total', 1)
  update_final(transaction.output5_name, 'output5_count', 'output5_total', 1)

  -- timestamp

  transaction.time = time()

  -- done

  local debug_item_name
  if transaction.output_name then
    debug_item_name = transaction.output_name
  else
    debug_item_name = transaction.input_name
  end

  if transaction.subtype then
    ChatFrame3:AddMessage(format('TRANSACTION %s/%s %s', transaction.type, transaction.subtype, debug_item_name))
  else
    ChatFrame3:AddMessage(format('TRANSACTION %s %s', transaction.type, debug_item_name))
  end

  table.insert(transaction_log, transaction)
end

-- auction tracking

-- track the number of items up for sale in our listings according to
-- minbid and buyout prices. auction house emails only list bid/buyout
-- prices so we will use them to guess the number sold. in cases with
-- multiple listings for the same price, the higher count is
-- kept. (the higher count gives more pessimistic sale prices and
-- rational buyers will prefer those listings.)

local function auction_owned_update()
  -- variable names from World of Warcraft Programming, page 529
  local numBatchAuctions, totalAuctions = GetNumAuctionItems("owner")
  if numBatchAuctions < totalAuctions then
    ChatFrame3:AddMessage('retrieved incomplete auction owner data')
    return
  end

  local new_owner_data = {}
  for i = 1,numBatchAuctions do
    -- variable names from World of Warcraft Programming, page 447
    local name, texture, count, quality, canUse, level, minBid, minIncrement, buyoutPrice, bidAmount, highBidder, owner = GetAuctionItemInfo("owner", i)

    -- sold auctions do not have count associated with them
    if count > 0 then
      if new_owner_data[name] == nil then
        new_owner_data[name] = {}
        new_owner_data[name]['bid'] = {}
        new_owner_data[name]['buyout'] = {}
      end
      local item_data = new_owner_data[name]

      local function update_count(t, p)
        if t[p] == nil then
          t[p] = count
        else
          t[p] = max(t[p], count)
        end
      end

      update_count(item_data['bid'], max(minBid, bidAmount))
      if buyoutPrice ~= nil then
        update_count(item_data['buyout'], buyoutPrice)
      end
    end
  end

  for item_name, item_data in pairs(new_owner_data) do
    if auction_owned_counts[item_name] == nil then
      auction_owned_counts[item_name] = {}
    end

    for price_type, price_data in pairs(item_data) do
      if auction_owned_counts[item_name][price_type] == nil then
        auction_owned_counts[item_name][price_type] = {}
      end

      for price, price_count in pairs(price_data) do
        auction_owned_counts[item_name][price_type][price] = price_count
      end
    end
  end
end

--

local function parse_chat_msg_loot(message)

  -- extract item name

  local item_name = get_item_name_from_item_link(message)

  -- figure out what kind of message

  local looted = false
  local created = false

  local item_count
  if string.find(message, string.gsub(string.gsub(LOOT_ITEM_PUSHED_SELF_MULTIPLE, '%%s', '.*', 1), '%%d', '(%%d+)', 1)) then
    local i, j, i_l, i_c = string.find(message, string.gsub(string.gsub(LOOT_ITEM_PUSHED_SELF_MULTIPLE, '%%s', '(.*)', 1), '%%d', '(%%d+)', 1))
    item_count = tonumber(i_c)
    looted = true
  elseif string.find(message, string.gsub(LOOT_ITEM_PUSHED_SELF, '%%s', '(.*)', 1)) then
    item_count = 1
    looted = true
  elseif string.find(message, string.gsub(string.gsub(LOOT_ITEM_CREATED_SELF_MULTIPLE, '%%s', '.*', 1), '%%d', '(%%d+)', 1)) then
    local i, j, i_l, i_c = string.find(message, string.gsub(string.gsub(LOOT_ITEM_CREATED_SELF_MULTIPLE, '%%s', '(.*)', 1), '%%d', '(%%d+)', 1))
    item_count = tonumber(i_c)
    created = true
  elseif string.find(message, string.gsub(LOOT_ITEM_CREATED_SELF, '%%s', '(.*)', 1)) then
    item_count = 1
    created = true
  else
    -- couldn't parse loot message
    return
  end
  
  -- record data

  -- created items are trivial to record
  if created then
    record_transaction({type = 'create', output_name = item_name, output_count = item_count}, false)
    return
  end

  -- merchant items
  local per_item_cost = merchant_per_item_costs[item_name]
  if per_item_cost ~= nil then
    record_transaction({type = 'buy', subtype = 'merchant', output_name = item_name, output_count = item_count, buy_price = item_count * per_item_cost}, false)
    return
  end

  -- no cost source found
end

-- invoice management (AH emails)

local function invoice_record(messageIndex, attachIndex)
  local bodyText, texture, isTakeable, isInvoice = GetInboxText(messageIndex)
  if not isInvoice then
    return
  end

  -- variable names from World of Warcraft Programming, page 499
  local invoiceType, itemName, playerName, bid, buyout, deposit, consignment, moneyDelay, etaHour, etaMin = GetInboxInvoiceInfo(messageIndex)

  local price
  if bid ~= nil then
    price = bid
  elseif buyout ~= nil then
    price = buyout
  end
  if price == nil then
    return
  end

  if invoiceType == "buyer" then
    -- variable names from World of Warcraft Programming, page 499
    local name, itemTexture, count, quality, canUse = GetInboxItem(messageIndex, attachIndex)

    record_transaction({type = 'buy', subtype = 'auction', output_name = itemName, output_count = count, buy_price = price}, false)

  elseif invoiceType == "seller" then
    local count
    if auction_owned_counts[itemName] ~= nil then
      -- if we have a bid, try to look up that bid
      count = auction_owned_counts[itemName]['bid'][bid]

      -- if we didn't find a bid match, compare bid/buyout against
      -- known buyouts. a bid may have been placed which matched a
      -- buyout.
      if count == nil then
        count = auction_owned_counts[itemName]['buyout'][price]
      end
    end

    if count == nil then
      ChatFrame3:AddMessage(format('unable to guess item count for %s @ %.4f', itemName, price / 10000))
    else
      record_transaction({type = 'sell', subtype = 'auction', input_name = itemName, input_count = count, sell_price = price}, true)
    end
  end
end

-- the point of this function is to read all the auction emails so
-- that all the invoice data is usable later.

local function invoice_scan()
  local inbox_size = GetInboxNumItems()

  for index = inbox_size,1,-1 do
    local packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, itemCount, wasRead, wasReturned, textCreated, canReply, isGM, itemQuantity = GetInboxHeaderInfo(index)    

    if (sender ~= nil) and string.find(sender, 'Auction House') then
      -- variable names from World of Warcraft Programming, page 498
      local bodyText, texture, isTakeable, isInvoice = GetInboxText(index)

      if isInvoice ~= nil then
        -- variable names from World of Warcraft Programming, page 499
        local invoiceType, itemName, playerName, bid, buyout, deposit, consignment, moneyDelay, etaHour, etaMin = GetInboxInvoiceInfo(index)
      end
    end
  end
end

-- looting - used to track milling 

local refine_input_counts = {}
refine_input_counts['Disenchant'] = 1
refine_input_counts['Milling'] = 5
refine_input_counts['Prospecting'] = 5

local function loot_scan()
  local num_loot_items = GetNumLootItems()
  if num_loot_items <= 0 then
    return
  end

  local input_count = refine_input_counts[last_spellcast_succeeded]
  if not input_count then
    return
  elseif last_item_lock == nil then
    return
  end

  local t = {type = 'refine', input_name = last_item_lock, input_count = input_count}
  t.subtype = string.lower(last_spellcast_succeeded)

  for i = 1,num_loot_items,1 do
    local suffix = ''
    if i > 1 then
      suffix = tostring(i)
    end

    -- http://wowprogramming.com/docs/api/GetLootSlotInfo
    local texture, item, quantity, quality, locked  = GetLootSlotInfo(i)
    t['output' .. suffix .. '_name'] = item
    t['output' .. suffix .. '_count'] = quantity
  end

  record_transaction(t, false)
  last_spellcast_succeeded = nil
end

-- merchant data

local function merchant_update()
  local num_items = GetMerchantNumItems()

  merchant_per_item_costs = {}
  for i = 1,num_items do
    -- variable names from World of Warcraft Programming, page 522
    local name, texture, price, quantity, numAvailable, isUsable, extendedCost = GetMerchantItemInfo(i)
    if name ~= nil then
      merchant_per_item_costs[name]  = price / quantity
    end
  end
end

-- event handling

local event_handlers = {}

event_handlers['AUCTION_OWNED_LIST_UPDATE'] = auction_owned_update

event_handlers['CHAT_MSG_LOOT'] = 
  function (self, event, ...)
    parse_chat_msg_loot(arg1)
  end

event_handlers['ITEM_LOCK_CHANGED'] =
  function (self, event, ...)
    if not arg2 then
      last_item_lock = nil
      return
    end

    if type(arg2) == 'boolean' then
      -- this happens when closing gem interface
      last_item_lock = nil
      return
    end

    local item_link = GetContainerItemLink(arg1, arg2)
    if item_link == nil then
      last_item_lock = nil
      return
    end

    local item_name = get_item_name_from_item_link(item_link)
    if item_name == nil then
      last_item_lock = nil
      return
    end

    last_item_lock = item_name
  end

event_handlers['LOOT_OPENED'] = loot_scan

event_handlers['MAIL_INBOX_UPDATE'] = invoice_scan
event_handlers['MAIL_SHOW'] = invoice_scan

event_handlers['MERCHANT_CLOSE'] = 
  function (self, event, ...)
    merchant_window = false
  end
event_handlers['MERCHANT_SHOW'] = 
  function (self, event, ...)
    merchant_window = true
    merchant_update()
  end
event_handlers['MERCHANT_UPDATE'] = merchant_update

event_handlers['UNIT_SPELLCAST_SUCCEEDED'] = 
  function (self, event, ...)
    if arg1 == 'player' then
      last_spellcast_succeeded = arg2
    end
  end

event_handlers['VARIABLES_LOADED'] = setup_variables

local frame = CreateFrame("Frame", "MoonSaberLedgerEventFrame")
frame:SetScript("OnEvent", 
                function (self, event, ...)
                  if type(event_handlers[event]) == 'function' then
                    event_handlers[event](self, event, ...)
                  end
                end)

for k, v in pairs(event_handlers) do
  frame:RegisterEvent(k)
end

-- hooked functions

local orig_AutoLootMailItem = AutoLootMailItem
function AutoLootMailItem(messageIndex, ...)
  invoice_record(messageIndex, 1)
  return orig_AutoLootMailItem(messageIndex, ...)
end

local orig_TakeInboxItem = TakeInboxItem
function TakeInboxItem(messageIndex, attachIndex, ...)
  invoice_record(messageIndex, attachIndex)
  return orig_TakeInboxItem(messageIndex, attachIndex, ...)
end
